#requires -version 5.1

[CmdletBinding()]
param(
    [ValidateRange(30, 3600)]
    [int]$TimeoutSeconds = 600
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$script:LogPath = "$env:ProgramData\Deploy-TeamViewer\Deploy-TeamViewer.log"

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Message,
        [ValidateSet('INFO', 'WARNING', 'ERROR')][string]$Level = 'INFO'
    )

    $line = '[{0:yyyy-MM-dd HH:mm:ss}] [{1}] {2}' -f (Get-Date), $Level, $Message

    # FileWave captures the native standard-output stream more reliably than
    # Write-Host, Write-Warning, or the native standard-error stream.
    Write-Output $line

    # Keep a second copy on the endpoint even if FileWave drops a stream.
    try {
        $logDirectory = Split-Path -Parent $script:LogPath
        if (-not (Test-Path -LiteralPath $logDirectory -PathType Container)) {
            New-Item -Path $logDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
        }
        Add-Content -LiteralPath $script:LogPath -Value $line -Encoding UTF8 -ErrorAction Stop
    }
    catch {
        try {
            Write-Output (
                '[{0:yyyy-MM-dd HH:mm:ss}] [WARNING] Could not write local log {1}: {2}' -f
                (Get-Date), $script:LogPath, $_.Exception.Message
            )
        }
        catch { }
    }
}

function Test-IsAdministrator {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-TeamViewerUninstallEntries {
    $entries = New-Object System.Collections.Generic.List[object]
    $registryLocations = @(
        @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; View = [Microsoft.Win32.RegistryView]::Registry64 },
        @{ Hive = [Microsoft.Win32.RegistryHive]::LocalMachine; View = [Microsoft.Win32.RegistryView]::Registry32 },
        @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser;  View = [Microsoft.Win32.RegistryView]::Registry64 },
        @{ Hive = [Microsoft.Win32.RegistryHive]::CurrentUser;  View = [Microsoft.Win32.RegistryView]::Registry32 }
    )

    foreach ($location in $registryLocations) {
        $baseKey = $null
        $uninstallKey = $null
        try {
            $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($location.Hive, $location.View)
            $uninstallKey = $baseKey.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
            if ($null -eq $uninstallKey) { continue }

            foreach ($subKeyName in $uninstallKey.GetSubKeyNames()) {
                $subKey = $null
                try {
                    $subKey = $uninstallKey.OpenSubKey($subKeyName)
                    $displayName = [string]$subKey.GetValue('DisplayName')
                    $publisher = [string]$subKey.GetValue('Publisher')

                    if ($displayName -match '(?i)TeamViewer' -or $publisher -match '(?i)^TeamViewer') {
                        $entries.Add([pscustomobject]@{
                            DisplayName          = $displayName
                            DisplayVersion       = [string]$subKey.GetValue('DisplayVersion')
                            KeyName              = $subKeyName
                            QuietUninstallString = [string]$subKey.GetValue('QuietUninstallString')
                            UninstallString      = [string]$subKey.GetValue('UninstallString')
                            InstallLocation      = [string]$subKey.GetValue('InstallLocation')
                            RegistryPath         = ('{0}\{1}\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{2}' -f $location.Hive, $location.View, $subKeyName)
                        })
                    }
                }
                finally {
                    if ($null -ne $subKey) { $subKey.Dispose() }
                }
            }
        }
        finally {
            if ($null -ne $uninstallKey) { $uninstallKey.Dispose() }
            if ($null -ne $baseKey) { $baseKey.Dispose() }
        }
    }

    return @($entries | Sort-Object RegistryPath -Unique)
}

function Get-TeamViewerServices {
    return @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue |
        Where-Object {
            $_.Name -match '(?i)TeamViewer' -or
            $_.DisplayName -match '(?i)TeamViewer' -or
            $_.PathName -match '(?i)[\\/]TeamViewer([\\/]|\.exe)'
        })
}

function Split-CommandLine {
    param([Parameter(Mandatory = $true)][string]$CommandLine)

    $expanded = [Environment]::ExpandEnvironmentVariables($CommandLine.Trim())
    if ($expanded -match '^\s*"([^"]+)"\s*(.*)$') {
        return [pscustomobject]@{ FilePath = $matches[1]; Arguments = $matches[2] }
    }
    if ($expanded -match '^\s*(.+?\.exe)\s*(.*)$') {
        return [pscustomobject]@{ FilePath = $matches[1].Trim(); Arguments = $matches[2] }
    }
    throw "Could not parse uninstall command: $CommandLine"
}

function Invoke-Uninstaller {
    param(
        [Parameter(Mandatory = $true)][string]$FilePath,
        [AllowEmptyString()][string]$Arguments = ''
    )

    Write-Log ('Running: "{0}" {1}' -f $FilePath, $Arguments)
    $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -PassThru -WindowStyle Hidden
    if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
        try { $process.Kill() } catch { }
        throw "Uninstaller timed out after $TimeoutSeconds seconds."
    }

    # MSI commonly uses 0 (success), 1605 (already absent), 1614 (already
    # uninstalled), 1641 (restart initiated), or 3010 (restart required).
    $successfulExitCodes = @(0, 1605, 1614, 1641, 3010)
    if ($process.ExitCode -notin $successfulExitCodes) {
        throw "Uninstaller returned exit code $($process.ExitCode)."
    }
}

function Remove-TeamViewerEntry {
    param([Parameter(Mandatory = $true)]$Entry)

    Write-Log ("Removing {0} {1}" -f $Entry.DisplayName, $Entry.DisplayVersion).Trim()

    if ($Entry.KeyName -match '^\{[0-9A-Fa-f-]{36}\}$' -or $Entry.UninstallString -match '(?i)\bmsiexec(?:\.exe)?\b') {
        $productCode = if ($Entry.KeyName -match '^\{[0-9A-Fa-f-]{36}\}$') {
            $Entry.KeyName
        }
        elseif ($Entry.UninstallString -match '(?i)\{[0-9A-Fa-f-]{36}\}') {
            $matches[0]
        }
        else {
            throw "An MSI uninstall entry was found, but its product code could not be determined."
        }

        Invoke-Uninstaller -FilePath "$env:SystemRoot\System32\msiexec.exe" -Arguments "/x $productCode /qn /norestart"
        return
    }

    if (-not [string]::IsNullOrWhiteSpace($Entry.QuietUninstallString)) {
        $command = Split-CommandLine -CommandLine $Entry.QuietUninstallString
        Invoke-Uninstaller -FilePath $command.FilePath -Arguments $command.Arguments
        return
    }

    if (-not [string]::IsNullOrWhiteSpace($Entry.UninstallString)) {
        $command = Split-CommandLine -CommandLine $Entry.UninstallString
        $arguments = $command.Arguments
        if ($arguments -notmatch '(?i)(^|\s)/(S|silent|quiet)(\s|$)') {
            $arguments = ($arguments + ' /S').Trim()
        }
        Invoke-Uninstaller -FilePath $command.FilePath -Arguments $arguments
        return
    }

    throw "No uninstall command was registered for $($Entry.DisplayName)."
}

function Get-TeamViewerFallbackUninstallers {
    $candidates = New-Object System.Collections.Generic.List[string]
    $roots = @(
        $env:ProgramFiles,
        ${env:ProgramFiles(x86)},
        $env:ProgramW6432
    ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique

    foreach ($root in $roots) {
        foreach ($relativePath in @(
            'TeamViewer\uninstall.exe',
            'TeamViewer\Uninstall.exe'
        )) {
            $path = Join-Path $root $relativePath
            if (Test-Path -LiteralPath $path -PathType Leaf) {
                $candidates.Add($path)
            }
        }
    }
    return @($candidates | Select-Object -Unique)
}

$exitCode = 1
try {
    Write-Log "Starting TeamViewer removal. Local log: $script:LogPath"
    Write-Log ('Execution identity: {0}; PowerShell: {1}; 64-bit process: {2}' -f
        [Security.Principal.WindowsIdentity]::GetCurrent().Name,
        $PSVersionTable.PSVersion,
        [Environment]::Is64BitProcess
    )

    Write-Log 'Inspecting 32-bit and 64-bit uninstall registry entries.'
    $initialEntries = @(Get-TeamViewerUninstallEntries)

    Write-Log 'Inspecting Windows services.'
    $initialServices = @(Get-TeamViewerServices)

    Write-Log 'Inspecting standard TeamViewer installation folders.'
    $fallbackUninstallers = @(Get-TeamViewerFallbackUninstallers)

    Write-Log ("Detection finished: {0} uninstall entry/entries, {1} service(s), and {2} fallback uninstaller(s)." -f
        $initialEntries.Count, $initialServices.Count, $fallbackUninstallers.Count
    )

    if ($initialEntries.Count -eq 0 -and $initialServices.Count -eq 0 -and $fallbackUninstallers.Count -eq 0) {
        Write-Log 'TeamViewer is not installed. No action is required.'
        $exitCode = 0
    }
    else {
        if (-not (Test-IsAdministrator)) {
            throw 'TeamViewer was detected, but this script is not running as an administrator.'
        }

        Write-Log ("Detected {0} uninstall entry/entries and {1} service(s)." -f $initialEntries.Count, $initialServices.Count)

        Get-Process -ErrorAction SilentlyContinue |
            Where-Object { $_.ProcessName -match '(?i)^TeamViewer' } |
            ForEach-Object {
                Write-Log "Stopping process $($_.ProcessName) (PID $($_.Id))."
                Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
            }

        foreach ($service in $initialServices) {
            if ($service.State -ne 'Stopped') {
                Write-Log "Stopping service $($service.Name)."
                Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
            }
        }

        $uninstallErrors = New-Object System.Collections.Generic.List[string]
        foreach ($entry in $initialEntries) {
            try {
                Remove-TeamViewerEntry -Entry $entry
            }
            catch {
                $uninstallErrors.Add("$($entry.DisplayName): $($_.Exception.Message)")
                Write-Log -Level 'WARNING' -Message ($uninstallErrors[$uninstallErrors.Count - 1])
            }
        }

        # This also covers damaged/legacy installations that have a service or
        # files present but no usable Add/Remove Programs entry.
        if ($initialEntries.Count -eq 0 -or $uninstallErrors.Count -gt 0) {
            foreach ($uninstaller in $fallbackUninstallers) {
                try {
                    Invoke-Uninstaller -FilePath $uninstaller -Arguments '/S'
                }
                catch {
                    $message = "Fallback uninstaller $uninstaller`: $($_.Exception.Message)"
                    $uninstallErrors.Add($message)
                    Write-Log -Level 'WARNING' -Message $message
                }
            }
        }

        # Require three consecutive clean checks. Some EXE uninstallers return
        # before their child cleanup process has completely stopped.
        $deadline = (Get-Date).AddSeconds([Math]::Min($TimeoutSeconds, 120))
        $cleanChecks = 0
        do {
            Start-Sleep -Seconds 2
            $remainingEntries = @(Get-TeamViewerUninstallEntries)
            $remainingServices = @(Get-TeamViewerServices)
            $remainingProcesses = @(Get-Process -ErrorAction SilentlyContinue |
                Where-Object { $_.ProcessName -match '(?i)^TeamViewer' })

            if ($remainingEntries.Count -eq 0 -and
                $remainingServices.Count -eq 0 -and
                $remainingProcesses.Count -eq 0) {
                $cleanChecks++
            }
            else {
                $cleanChecks = 0
            }
        } while ($cleanChecks -lt 3 -and (Get-Date) -lt $deadline)

        if ($cleanChecks -lt 3) {
            $entryNames = @($remainingEntries | ForEach-Object { $_.DisplayName }) -join ', '
            $serviceNames = @($remainingServices | ForEach-Object { $_.Name }) -join ', '
            $processNames = @($remainingProcesses | ForEach-Object { $_.ProcessName }) -join ', '
            throw "TeamViewer removal could not be verified. Remaining uninstall entries: [$entryNames]. Remaining services: [$serviceNames]. Remaining processes: [$processNames]."
        }

        Write-Log 'TeamViewer was removed successfully and no installed instance remains.'
        $exitCode = 0
    }
}
catch {
    Write-Log -Level 'ERROR' -Message ("TeamViewer removal failed: {0}: {1}" -f
        $_.Exception.GetType().FullName,
        $_.Exception.Message
    )

    if (-not [string]::IsNullOrWhiteSpace($_.InvocationInfo.PositionMessage)) {
        Write-Log -Level 'ERROR' -Message ("Failure location: {0}" -f $_.InvocationInfo.PositionMessage.Trim())
    }
    if (-not [string]::IsNullOrWhiteSpace($_.ScriptStackTrace)) {
        Write-Log -Level 'ERROR' -Message ("PowerShell stack: {0}" -f $_.ScriptStackTrace.Trim())
    }

    Write-Log -Level 'ERROR' -Message "Full diagnostic log: $script:LogPath"
    $exitCode = 1
}

if ($exitCode -ne 0) {
    Write-Log -Level 'ERROR' -Message 'Installation will not run because removal did not complete successfully.'
    exit $exitCode
}

# Installation begins only after removal has succeeded and remained absent for
# three consecutive checks above.
try {
    if (-not (Test-IsAdministrator)) {
        throw 'TeamViewer installation requires administrator privileges.'
    }

    $installerDirectory = 'C:\ProgramData\FileWave\Installers\TeamViewer'
    $msiPath = Join-Path $installerDirectory 'TeamViewer_Host.msi'
    $tvoptFile = Join-Path $installerDirectory 'custom.tvopt'
    $tvoptExample = Join-Path $installerDirectory 'example_custom.tvopt'

    if (-not (Test-Path -LiteralPath $msiPath -PathType Leaf)) {
        throw "TeamViewer installer was not found at $msiPath."
    }

    # Hide the unattended-access wizard for the new installation.
    $regPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\TeamViewer'
    New-Item -Path $regPath -Force -ErrorAction Stop | Out-Null
    Set-ItemProperty -Path $regPath -Name 'UnattendedAccessWizardShown' -Value 1 -Type DWord -ErrorAction Stop

    $msiArguments = @(
        '/i'
        "`"$msiPath`""
        '/qn'
        '/norestart'
        'DESKTOPSHORTCUTS=0'
        'CUSTOMCONFIGID=65uhtxf'
    )

    if (Test-Path -LiteralPath $tvoptFile -PathType Leaf) {
        $msiArguments += "SETTINGSFILE=`"$tvoptFile`""
        Write-Log "Using TeamViewer settings file: $tvoptFile"
    }
    elseif (Test-Path -LiteralPath $tvoptExample -PathType Leaf) {
        $msiArguments += "SETTINGSFILE=`"$tvoptExample`""
        Write-Log "Using TeamViewer settings file: $tvoptExample"
    }

    Write-Log "Installing TeamViewer Host from $msiPath."

    # If another Windows Installer transaction is briefly finishing, retry
    # instead of allowing MSI error 1618 to make this deployment intermittent.
    $installDeadline = (Get-Date).AddSeconds($TimeoutSeconds)
    do {
        $installProcess = Start-Process -FilePath "$env:SystemRoot\System32\msiexec.exe" `
            -ArgumentList $msiArguments -PassThru -WindowStyle Hidden

        if (-not $installProcess.WaitForExit($TimeoutSeconds * 1000)) {
            try { $installProcess.Kill() } catch { }
            throw "TeamViewer installation timed out after $TimeoutSeconds seconds."
        }

        $installExitCode = $installProcess.ExitCode
        if ($installExitCode -eq 1618 -and (Get-Date) -lt $installDeadline) {
            Write-Log -Level 'WARNING' -Message 'Windows Installer is still busy (1618); retrying in 10 seconds.'
            Start-Sleep -Seconds 10
        }
    } while ($installExitCode -eq 1618 -and (Get-Date) -lt $installDeadline)

    if ($installExitCode -notin @(0, 3010)) {
        throw "TeamViewer installation returned exit code $installExitCode."
    }

    if ($installExitCode -eq 3010) {
        Write-Log 'TeamViewer installed successfully; Windows Installer reported that a restart is required.'
    }
    else {
        Write-Log 'TeamViewer installed successfully.'
    }

    exit 0
}
catch {
    Write-Log -Level 'ERROR' -Message ("TeamViewer installation failed: {0}: {1}" -f
        $_.Exception.GetType().FullName,
        $_.Exception.Message
    )
    Write-Log -Level 'ERROR' -Message "Full diagnostic log: $script:LogPath"
    exit 1
}